﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using log4net;

namespace HttpTunnel
{
    class Tunneler
    {
        private static readonly ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        private const int BufferSize = 8192;
        public static IPEndPoint RemoteIPEndPoint { get; private set; }
        public static string DefaultUserAgent { get; private set; }



        public static void Initialize(Config config)
        {
            RemoteIPEndPoint = config.Proxy;
            DefaultUserAgent = config.UserAgent;

            ResponseSender.Initialize(config);
            RequestProcesser.Initialize(config);
        }


        public static void StartHandshake(Socket client)
        {
            var userState = new RequestUserState { Buffer = new byte[BufferSize], Client = client };

            try
            {
                client.BeginReceive(userState.Buffer, 0, userState.Buffer.Length, SocketFlags.None, OnReceivedQuery, userState);
            }
            catch (Exception ex)
            {
                Logger.Debug("StartHandshake", ex);

                client.Close();
            }
        }


        private static void OnReceivedQuery(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RequestUserState;

            try
            {
                var ret = userState.Client.EndReceive(ar);

                if (ret <= 0 && userState.Request.Length == 0)
                {
                    userState.Client.Close();
                    return;
                }

                userState.Request += Encoding.ASCII.GetString(userState.Buffer, 0, ret);

                if (ret == BufferSize)//havn't finished, keep receiving...
                {
                    userState.Client.BeginReceive(userState.Buffer, 0, userState.Buffer.Length, SocketFlags.None, OnReceivedQuery, userState);
                }
                else
                {
                    IPEndPoint remoteIPEndPoint;
                    bool isKeepAlive;
                    UrlValidateResult urlValidateResult;
                    var isInternalRequest = RequestProcesser.IsInternalRequest(userState.Request, out urlValidateResult, out remoteIPEndPoint, out isKeepAlive);

                    //if it's internal request, forward the request. 
                    if (isInternalRequest)
                    {
                        userState.Request = userState.Request;
                        userState.UrlValidateResult = urlValidateResult;
                        userState.Remote = new Socket(remoteIPEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                        if (isKeepAlive)
                        {
                            userState.Remote.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1);
                        }
                        userState.Remote.BeginConnect(remoteIPEndPoint, OnConnected, userState);
                    }
                    else
                    {
                        var result = RequestProcesser.IsValid(userState.Request, out urlValidateResult);
                        if (result == RequestValidateResult.OK)
                        {
                            userState.Request = RequestProcesser.RebuildRequest(userState.Request);
                            userState.UrlValidateResult = urlValidateResult;
                            userState.Remote = new Socket(RemoteIPEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                            userState.Remote.BeginConnect(RemoteIPEndPoint, OnConnected, userState);
                        }
                        else
                        {
                            ProcessFailedRequest(result, userState.Request, userState.Client);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Debug("OnReceivedQuery", ex);

                userState.Client.Close();
            }
        }

        private static bool ProcessFailedRequest(RequestValidateResult result, string request, Socket socket)
        {
            switch (result)
            {
                case RequestValidateResult.OK:
                    return true;
                case RequestValidateResult.Warning:
                    Logger.Warn(new RequestDetail { RemoteIP = ((IPEndPoint)socket.RemoteEndPoint).Address.ToString(), Content = request }.ToString());
                    ResponseSender.SendBadRequest(socket);
                    return false;
                case RequestValidateResult.ErrorKeywords:
                    Logger.Error(new RequestDetail { RemoteIP = ((IPEndPoint)socket.RemoteEndPoint).Address.ToString(), Content = request, Comment = "Keywords" }.ToString());
                    ResponseSender.SendBadRequest(socket);
                    return false;
                case RequestValidateResult.BrowserOutOfDate:
                    Logger.Warn(new RequestDetail { RemoteIP = ((IPEndPoint)socket.RemoteEndPoint).Address.ToString(), Comment = "iSafe Browser is out of date." }.ToString());
                    ResponseSender.SendOutOfDate(socket);
                    return false;
                default:
                    return false;
            }
        }


        private static void OnConnected(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RequestUserState;

            try
            {
                userState.Remote.EndConnect(ar);

                var requestBuffer = Encoding.ASCII.GetBytes(userState.Request);

                userState.Remote.BeginSend(requestBuffer, 0, requestBuffer.Length, SocketFlags.None, OnQuerySent, userState);
            }
            catch (Exception ex)
            {
                Logger.Debug("OnConnected", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }


        private static void OnQuerySent(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RequestUserState;

            try
            {
                var ret = userState.Remote.EndSend(ar);

                if (ret <= 0)
                {
                    userState.Remote.Close();
                    return;
                }

                //StartRelay

                var userState1 = new RelayUserState
                    {
                        Buffer = new byte[BufferSize],
                        Client = userState.Client,
                        Remote = userState.Remote,
                        UrlValidateResult = userState.UrlValidateResult
                    };

                userState1.Client.BeginReceive(userState1.Buffer, 0, userState1.Buffer.Length, SocketFlags.None, OnClientReceived, userState1);


                var userState2 = new RelayUserState
                    {
                        Buffer = new byte[BufferSize],
                        Client = userState.Client,
                        Remote = userState.Remote,
                        UrlValidateResult = userState.UrlValidateResult
                    };
                userState2.Remote.BeginReceive(userState2.Buffer, 0, userState2.Buffer.Length, SocketFlags.None, OnRemoteReceived, userState2);
            }
            catch (Exception ex)
            {
                Logger.Debug("OnQuerySent", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }



        private static void OnClientReceived(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RelayUserState;

            try
            {
                var ret = userState.Client.EndReceive(ar);
                if (ret <= 0)
                {
                    userState.Client.Close();
                    return;
                }

                //as a keep-alive request
                var content = Encoding.ASCII.GetString(userState.Buffer, 0, ret);
                RequestValidateResult result;

                if (userState.UrlValidateResult == UrlValidateResult.WhiteList || userState.UrlValidateResult == UrlValidateResult.InternalList)
                {
                    result = RequestValidateResult.OK;
                }
                else
                {
                    result = RequestProcesser.IsValidKeepAlive(content);
                }

                if (result == RequestValidateResult.OK)
                {
                    userState.Remote.BeginSend(userState.Buffer, 0, ret, SocketFlags.None, OnRemoteSent, userState);
                }
                else
                {
                    ProcessFailedRequest(result, content, userState.Client);
                }
            }
            catch (Exception ex)
            {
                Logger.Debug("OnClientReceived", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }

        private static void OnRemoteSent(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RelayUserState;

            try
            {
                var ret = userState.Remote.EndSend(ar);

                if (ret <= 0)
                {
                    return;
                }

                var userState1 = new RelayUserState
                    {
                        Buffer = new byte[BufferSize],
                        Client = userState.Client,
                        Remote = userState.Remote
                    };
                userState1.Client.BeginReceive(userState1.Buffer, 0, userState1.Buffer.Length, SocketFlags.None, OnClientReceived, userState1);
            }
            catch (Exception ex)
            {
                Logger.Debug("OnRemoteSent", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }


        private static void OnRemoteReceived(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RelayUserState;

            try
            {
                var ret = userState.Remote.EndReceive(ar);

                if (ret <= 0)
                {
                    userState.Remote.Close();
                    return;
                }

                userState.Client.BeginSend(userState.Buffer, 0, ret, SocketFlags.None, OnClientSent, userState);
            }
            catch (Exception ex)
            {
                Logger.Debug("OnRemoteReceived", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }

        private static void OnClientSent(IAsyncResult ar)
        {
            var userState = ar.AsyncState as RelayUserState;

            try
            {
                var ret = userState.Client.EndSend(ar);

                if (ret <= 0)
                {
                    return;
                }

                var userState1 = new RelayUserState
                    {
                        Buffer = new byte[BufferSize],
                        Client = userState.Client,
                        Remote = userState.Remote
                    };
                userState1.Remote.BeginReceive(userState1.Buffer, 0, userState1.Buffer.Length, SocketFlags.None, OnRemoteReceived, userState1);
            }
            catch (Exception ex)
            {
                Logger.Debug("OnClientSent", ex);

                userState.Client.Close();
                userState.Remote.Close();
            }
        }
    }
}
